package com.shiwaixiangcun.authz.service.impl;

import com.shiwaixiangcun.authz.oauth.AuthenticationIdGenerator;
import com.shiwaixiangcun.authz.service.OauthService;
import com.shiwaixiangcun.core.domain.account.Account;
import com.shiwaixiangcun.core.domain.oauth.AccessToken;
import com.shiwaixiangcun.core.domain.oauth.ClientDetails;
import com.shiwaixiangcun.core.domain.oauth.OauthCode;
import com.shiwaixiangcun.core.repository.oauth.AccessTokenRepository;
import com.shiwaixiangcun.core.repository.oauth.ClientDetailsRepository;
import com.shiwaixiangcun.core.repository.oauth.OauthCodeRepository;
import com.shiwaixiangcun.utils.MonkeyUtils;
import org.apache.oltu.oauth2.as.issuer.OAuthIssuer;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.utils.OAuthUtils;
import org.apache.shiro.SecurityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Set;

/**
 * Created by SilentWu on 2017/2/24.
 */
@Service
@Transactional
public class OauthServiceImpl implements OauthService {
    private static final Logger LOG = LoggerFactory.getLogger(OauthServiceImpl.class);

    @Autowired
    private ClientDetailsRepository clientDetailsRepository;
    @Autowired
    private OauthCodeRepository oauthCodeRepository;
    @Autowired
    private AuthenticationIdGenerator authenticationIdGenerator;
    @Autowired
    private AccessTokenRepository accessTokenRepository;
    @Autowired
    private OAuthIssuer oAuthIssuer;

    @Override
    @Transactional(readOnly = true)
    public ClientDetails loadClientDetails(String clientId) {
        LOG.debug("Load ClientDetails by clientId: {}", clientId);
        return clientDetailsRepository.findByClientId(clientId);
    }

    @Override
    public OauthCode saveAuthorizationCode(String authCode, ClientDetails clientDetails) {
        final Account account = currentAccount();
        OauthCode oauthCode = new OauthCode();
        oauthCode.setCode(authCode);
        oauthCode.setAccount(account.getAccount());
        oauthCode.setClientId(clientDetails.getClientId());

        oauthCodeRepository.save(oauthCode);
        LOG.debug("Save OauthCode: {}", oauthCode);
        return oauthCode;
    }


    @Override
    public String retrieveAuthCode(ClientDetails clientDetails) throws OAuthSystemException {
        final String clientId = clientDetails.getClientId();
        final Account account = currentAccount();

        List<OauthCode> oauthCodes = oauthCodeRepository.findByClientIdAndAccount(clientId, account.getAccount());
        if (MonkeyUtils.isNotEmpty(oauthCodes)) {
            //Always delete exist
            LOG.debug("OauthCode ({}) is existed, remove it and create a new one", oauthCodes.toString());
            oauthCodeRepository.delete(oauthCodes);
        }
        //create a new one
        OauthCode oauthCode = createOauthCode(clientDetails);

        return oauthCode.getCode();
    }

    private Account currentAccount() {
        return (Account) SecurityUtils.getSubject().getPrincipal();
    }


    private OauthCode createOauthCode(ClientDetails clientDetails) throws OAuthSystemException {
        OauthCode oauthCode;
        final String authCode = oAuthIssuer.authorizationCode();

        LOG.debug("Save authorizationCode '{}' of ClientDetails '{}'", authCode, clientDetails);
        oauthCode = this.saveAuthorizationCode(authCode, clientDetails);
        return oauthCode;
    }

    @Override
    public AccessToken retrieveAccessToken(ClientDetails clientDetails, Set<String> scopes, boolean includeRefreshToken) throws OAuthSystemException {
        String scope = OAuthUtils.encodeScopes(scopes);
        final Account account = currentAccount();
        final String clientId = clientDetails.getClientId();

        final String authenticationId = authenticationIdGenerator.generate(clientId, account.getAccount(), scope);

        AccessToken accessToken = accessTokenRepository.findByClientIdAndAccountAndAuthenticationId(clientId, account.getAccount(), authenticationId);
        if (accessToken == null) {
            accessToken = createAndSaveAccessToken(clientDetails, includeRefreshToken, account.getAccount(), authenticationId);
            LOG.debug("Create a new AccessToken: {}", accessToken);
        }

        return accessToken;
    }

    @Override
    public AccessToken retrieveNewAccessToken(ClientDetails clientDetails, Set<String> scopes) throws OAuthSystemException {
        String scope = OAuthUtils.encodeScopes(scopes);
        final Account account = currentAccount();
        final String clientId = clientDetails.getClientId();

        final String authenticationId = authenticationIdGenerator.generate(clientId, account.getAccount(), scope);

        AccessToken accessToken = accessTokenRepository.findByClientIdAndAccountAndAuthenticationId(clientId, account.getAccount(), authenticationId);
        if (accessToken != null) {
            LOG.debug("Delete existed AccessToken: {}", accessToken);
            accessTokenRepository.delete(accessToken);
        }
        accessToken = createAndSaveAccessToken(clientDetails, false, account.getAccount(), authenticationId);
        LOG.debug("Create a new AccessToken: {}", accessToken);

        return accessToken;
    }

    @Override
    @Transactional(readOnly = true)
    public OauthCode loadOauthCode(String code, ClientDetails clientDetails) {
        final String clientId = clientDetails.getClientId();
        return oauthCodeRepository.findByCodeAndClientId(code, clientId);
    }

    @Override
    public boolean removeOauthCode(String code, ClientDetails clientDetails) {
        final OauthCode oauthCode = loadOauthCode(code, clientDetails);
        final int rows = oauthCodeRepository.deleteByCodeAndAccountAndClientId(oauthCode.getCode(), oauthCode.getAccount(), oauthCode.getClientId());
        return rows > 0;
    }

    @Override
    public AccessToken retrieveAuthorizationCodeAccessToken(ClientDetails clientDetails, String code) throws OAuthSystemException {
        final OauthCode oauthCode = loadOauthCode(code, clientDetails);
        final String account = oauthCode.getAccount();
        final String clientId = clientDetails.getClientId();

        final String authenticationId = authenticationIdGenerator.generate(clientId, account, null);

        AccessToken accessToken = accessTokenRepository.findByClientIdAndAccountAndAuthenticationId(clientId, account, authenticationId);
        if (accessToken != null) {
            LOG.debug("Delete existed AccessToken: {}", accessToken);
            accessTokenRepository.deleteByClientIdAndAccountAndAuthenticationId(accessToken.getClientId(), accessToken.getAccount(), accessToken.getAuthenticationId());
        }
        accessToken = createAndSaveAccessToken(clientDetails, clientDetails.supportRefreshToken(), account, authenticationId);
        LOG.debug("Create a new AccessToken: {}", accessToken);

        return accessToken;
    }

    private AccessToken createAndSaveAccessToken(ClientDetails clientDetails, boolean includeRefreshToken, String account, String authenticationId) throws OAuthSystemException {
        AccessToken accessToken = new AccessToken();
        accessToken.setClientId(clientDetails.getClientId());
        accessToken.setAccount(account);
        accessToken.setTokenId(oAuthIssuer.accessToken());
        accessToken.setAuthenticationId(authenticationId);
        accessToken.updateByClientDetails(clientDetails);
        if ("servicecenter".equals(clientDetails.getScope())) {
            accessToken.setTokenExpiredSeconds(86400);
        }

        if (includeRefreshToken) {
            accessToken.setRefreshToken(oAuthIssuer.refreshToken());
        }

        this.accessTokenRepository.save(accessToken);
        LOG.debug("Save AccessToken: {}", accessToken);
        return accessToken;
    }

    //grant_type=password AccessToken
    @Override
    public AccessToken retrievePasswordAccessToken(ClientDetails clientDetails, Set<String> scopes, String account) throws OAuthSystemException {
        String scope = OAuthUtils.encodeScopes(scopes);
        final String clientId = clientDetails.getClientId();

        final String authenticationId = authenticationIdGenerator.generate(clientId, account, scope);
        AccessToken accessToken = accessTokenRepository.findByClientIdAndAccountAndAuthenticationId(clientId, account, authenticationId);

        boolean needCreate = false;
        if (accessToken == null) {
            needCreate = true;
            LOG.debug("Not found AccessToken from repository, will create a new one, client_id: {}", clientId);
        } else if (accessToken.tokenExpired()) {
            LOG.debug("Delete expired AccessToken: {} and create a new one, client_id: {}", accessToken, clientId);
            accessTokenRepository.delete(accessToken);
            needCreate = true;
        } else {
            LOG.debug("Use existed AccessToken: {}, client_id: {}", accessToken, clientId);
        }

        if (needCreate) {
            accessToken = createAndSaveAccessToken(clientDetails, clientDetails.supportRefreshToken(), account, authenticationId);
            LOG.debug("Create a new AccessToken: {}", accessToken);
        }

        return accessToken;
    }

    //grant_type=client_credentials
    @Override
    public AccessToken retrieveClientCredentialsAccessToken(ClientDetails clientDetails, Set<String> scopes) throws OAuthSystemException {
        String scope = OAuthUtils.encodeScopes(scopes);
        final String clientId = clientDetails.getClientId();
        //username = clientId

        final String authenticationId = authenticationIdGenerator.generate(clientId, clientId, scope);
        AccessToken accessToken = accessTokenRepository.findByClientIdAndAccountAndAuthenticationId(clientId, clientId, authenticationId);

        boolean needCreate = false;
        if (accessToken == null) {
            needCreate = true;
            LOG.debug("Not found AccessToken from repository, will create a new one, client_id: {}", clientId);
        } else if (accessToken.tokenExpired()) {
            LOG.debug("Delete expired AccessToken: {} and create a new one, client_id: {}", accessToken, clientId);
            accessTokenRepository.delete(accessToken);
            needCreate = true;
        } else {
            LOG.debug("Use existed AccessToken: {}, client_id: {}", accessToken, clientId);
        }

        if (needCreate) {
            //Ignore refresh_token
            accessToken = createAndSaveAccessToken(clientDetails, false, clientId, authenticationId);
            LOG.debug("Create a new AccessToken: {}", accessToken);
        }

        return accessToken;
    }

    @Override
    public AccessToken loadAccessTokenByRefreshToken(String refreshToken, String clientId) {
        LOG.debug("Load ClientDetails by refreshToken: {} and clientId: {}", refreshToken, clientId);
        return accessTokenRepository.findByRefreshTokenAndClientId(refreshToken, clientId);
    }

    /*
     * Get AccessToken
     * Generate a new AccessToken from existed(exclude token,refresh_token)
     * Update access_token,refresh_token, expired.
     * Save and remove old
     * */
    @Override
    public AccessToken changeAccessTokenByRefreshToken(String refreshToken, String clientId) throws OAuthSystemException {
        final AccessToken oldToken = loadAccessTokenByRefreshToken(refreshToken, clientId);

        AccessToken newAccessToken = oldToken.cloneMe();
        LOG.debug("Create new AccessToken: {} from old AccessToken: {}", newAccessToken, oldToken);

        ClientDetails details = clientDetailsRepository.findByClientId(clientId);
        newAccessToken.updateByClientDetails(details);

        final String authId = authenticationIdGenerator.generate(clientId, oldToken.getAccount(), null);
        newAccessToken.setAuthenticationId(authId);
        newAccessToken.setTokenId(oAuthIssuer.accessToken());
        newAccessToken.setRefreshToken(oAuthIssuer.refreshToken());

        accessTokenRepository.delete(oldToken);
        LOG.debug("Delete old AccessToken: {}", oldToken);

        accessTokenRepository.save(newAccessToken);
        LOG.debug("Save new AccessToken: {}", newAccessToken);

        return newAccessToken;
    }

    @Override
    public ClientDetails loadClientDetailsByTokenId(String tokenId) {
        AccessToken accessToken = accessTokenRepository.findByTokenId(tokenId);
        return clientDetailsRepository.findByClientId(accessToken.getClientId());
    }

}
